# python3
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provide abstractions around concepts from the Rust compiler.

This module provides abstractions around the compiler's targets (build
architectures), and mappings between those architectures and GN conditionals."""

from __future__ import annotations

from enum import Enum
import re


class ArchSet:
    """A set of compiler target architectures.

    This is used to track the output of `cargo` or `rustc` and match it against
    different architectures.
    """

    def __init__(self, initial: set[str]):
        for a in initial:
            assert a in _RUSTC_ARCH_TO_BUILD_CONDITION
        # Internally stored as a set of architecture strings.
        self._set = initial

    def add_archset(self, other: ArchSet) -> bool:
        """Makes `self` into the union of `self` and `other`.

        Returns if anything was added to the ArchSet."""
        if self._set.issuperset(other._set):
            return False
        self._set.update(other._set)
        return True

    def has_arch(self, arch: str) -> bool:
        """Whether the `ArchSet` contains `arch`."""
        return arch in self._set

    def as_strings(self) -> set[str]:
        """Returns `self` as a raw set of strings."""
        return self._set

    def __bool__(self) -> bool:
        """Whether the `ArchSet` is non-empty."""
        return bool(self._set)

    def __len__(self) -> int:
        """The number of architectures in the `ArchSet`."""
        return len(self._set)

    def __repr__(self) -> str:
        """A string representation of the `ArchSet`."""
        return "ArchSet({})".format(repr(self._set))

    def __eq__(self, other: ArchSet) -> bool:
        """Whether `self` and `other` contain the same architectures."""
        return self._set == other._set

    def __and__(self, other: ArchSet) -> ArchSet:
        """An intersection of `self` and `other`.

        Returns a new `ArchSet` that contains only the architectures that are
        present in both `self` and `other`."""
        return ArchSet(initial=(self._set & other._set))

    @staticmethod
    def ALL() -> ArchSet:
        """All valid architectures."""
        return ArchSet({k for k in _RUSTC_ARCH_TO_BUILD_CONDITION.keys()})

    @staticmethod
    def ONE() -> ArchSet:
        """Arbitrary selection of a single architecture."""
        return ArchSet({"aarch64-apple-ios"})

    @staticmethod
    def EMPTY() -> ArchSet:
        """No architectures."""
        return ArchSet(set())


class BuildCondition(Enum):
    """Each value corresponds to a BUILD file condition that can be branched on.

    These flags currently store the GN condition statements directly, to easily
    convert from a `BuildCondition` to a GN if-statement.
    """
    # For Android builds, these come from `android_abi_target` values in
    # //build/config/android/abi.gni.
    ANDROID_X86 = "is_android && target_cpu == \"x86\"",
    ANDROID_X64 = "is_android && target_cpu == \"x64\"",
    ANDROID_ARM = "is_android && target_cpu == \"arm\"",
    ANDROID_ARM64 = "is_android && target_cpu == \"arm64\"",
    # Not supported by rustc but is in //build/config/android/abi.gni
    #   ANDROID_MIPS = "is_android && target_cpu == \"mipsel\"",
    # Not supported by rustc but is in //build/config/android/abi.gni
    #   ANDROID_MIPS64 = "is_android && target_cpu == \"mips64el\"",
    # For Fuchsia builds, these come from //build/config/rust.gni.
    FUCHSIA_ARM64 = "is_fuchsia && target_cpu == \"arm64\"",
    FUCHSIA_X64 = "is_fuchsia && target_cpu == \"x64\"",
    # For iOS builds, these come from //build/config/rust.gni.
    IOS_ARM64 = "is_ios && target_cpu == \"arm64\"",
    IOS_ARM = "is_ios && target_cpu == \"arm\"",
    IOS_X64 = "is_ios && target_cpu == \"x64\"",
    IOS_X86 = "is_ios && target_cpu == \"x86\"",
    WINDOWS_X86 = "is_win && target_cpu == \"x86\"",
    WINDOWS_X64 = "is_win && target_cpu == \"x64\"",
    LINUX_X86 = "(is_linux || is_chromeos) && target_cpu == \"x86\"",
    LINUX_X64 = "(is_linux || is_chromeos) && target_cpu == \"x64\"",
    MAC_X64 = "is_mac && target_cpu == \"x64\"",
    MAC_ARM64 = "is_mac && target_cpu == \"arm64\"",
    # Combinations generated by BuildConditionSet._optimize()
    ALL_ARM32 = "target_cpu == \"arm\"",
    ALL_ARM64 = "target_cpu == \"arm64\"",
    ALL_X64 = "target_cpu == \"x64\"",
    ALL_X86 = "target_cpu == \"x86\"",
    ALL_ANDROID = "is_android",
    ALL_FUCHSIA = "is_fuchsia",
    ALL_IOS = "is_ios",
    ALL_WINDOWS = "is_win",
    ALL_LINUX = "is_linux || is_chromeos",
    ALL_MAC = "is_mac",
    NOT_ANDROID = "!is_android",
    NOT_FUCHSIA = "!is_fuchsia",
    NOT_IOS = "!is_ios",
    NOT_WINDOWS = "!is_win",
    NOT_LINUX = "!(is_linux || is_chromeos)",
    NOT_MAC = "!is_mac",

    def gn_condition(self) -> str:
        """Gets the GN conditional text that represents the `BuildCondition`."""
        return self.value[0]


class BuildConditionSet:
    """A group of conditions for which the BUILD file can branch on.

    The conditions are each OR'd together, that is the set combines a group of
    conditions where any one of the conditions would be enough to satisfy the
    set.

    The group of conditions is built from an ArchSet, but provides a separate
    abstraction as it can be optimized to combine `BuildCondition`s, in order to
    cover multiple BUILD file conditions with fewer, more general conditions.

    An empty BuildConditionSet is never true, so a BUILD file output that would
    be conditional on such a set should be skipped entirely.
    """

    def __init__(self, arch_set: ArchSet):
        self.arch_set = arch_set

    def is_always_true(self):
        """Whether the set covers all possible BUILD file configurations."""
        return len(self.arch_set) == len(ArchSet.ALL())

    def inverted(self):
        inverse: set[str] = ArchSet.ALL().as_strings(
        ) - self.arch_set.as_strings()
        return BuildConditionSet(ArchSet(initial=inverse))

    def get_gn_conditions(self):
        """Generate the set of BUILD file conditions as text.

        Returns:
            A set of GN conditions (as strings) that should be evaluated.
            The result should be true if any of them is true.

            An empty set is returned to indicate there are no conditions.
        """
        # No arches are covered! We should not use this BuildConditionSet
        # to generate any output as it would always be `false`.
        assert self.arch_set, ("Generating BUILD rules for an empty "
                               "BuildConditionSet (which is never true).")

        if self.is_always_true():
            return []  # All archs are covered, so no conditions needed.

        modes = {
            _RUSTC_ARCH_TO_BUILD_CONDITION[a]
            for a in self.arch_set.as_strings()
        }
        return [m.gn_condition() for m in self._optimize(modes)]

    def _optimize(self, modes: set[BuildCondition]) -> set[BuildCondition]:
        """Combine `BuildCondition`s into a smaller, more general set.

        Args:
            modes: A set of BuildConditions to optimize.

        Returns:
            A smaller set of BuildConditions, if it's possible to optimize, or
            the original `modes` set.
        """

        def build_cond(arch: str) -> BuildCondition:
            return _RUSTC_ARCH_TO_BUILD_CONDITION[arch]

        def build_conds_matching(matching: str) -> set[BuildCondition]:
            return {
                build_cond(arch)
                for arch in _RUSTC_ARCH_TO_BUILD_CONDITION
                if re.search(matching, arch)
            }

        # Defines a set of modes we can collapse more verbose modes down into.
        # For each pair, if all of the modes in the 2nd position are present,
        # we can replace them all with the mode in the 1st position.
        os_combinations: list[tuple[BuildCondition, set[BuildCondition]]] = [
            (BuildCondition.ALL_IOS,
             build_conds_matching(_RUSTC_ARCH_MATCH_IOS)),
            (BuildCondition.ALL_WINDOWS,
             build_conds_matching(_RUSTC_ARCH_MATCH_WINDOWS)),
            (BuildCondition.ALL_LINUX,
             build_conds_matching(_RUSTC_ARCH_MATCH_LINUX)),
            (BuildCondition.ALL_MAC,
             build_conds_matching(_RUSTC_ARCH_MATCH_MAC)),
            (BuildCondition.ALL_ANDROID,
             build_conds_matching(_RUSTC_ARCH_MATCH_ANDROID)),
            (BuildCondition.ALL_FUCHSIA,
             build_conds_matching(_RUSTC_ARCH_MATCH_FUCHSIA)),
        ]
        os_merges: list[tuple[BuildCondition, set[BuildCondition]]] = [
            (BuildCondition.NOT_ANDROID, {
                BuildCondition.ALL_FUCHSIA, BuildCondition.ALL_IOS,
                BuildCondition.ALL_WINDOWS, BuildCondition.ALL_LINUX,
                BuildCondition.ALL_MAC
            }),
            (BuildCondition.NOT_FUCHSIA, {
                BuildCondition.ALL_ANDROID, BuildCondition.ALL_IOS,
                BuildCondition.ALL_WINDOWS, BuildCondition.ALL_LINUX,
                BuildCondition.ALL_MAC
            }),
            (BuildCondition.NOT_IOS, {
                BuildCondition.ALL_ANDROID, BuildCondition.ALL_FUCHSIA,
                BuildCondition.ALL_WINDOWS, BuildCondition.ALL_LINUX,
                BuildCondition.ALL_MAC
            }),
            (BuildCondition.NOT_WINDOWS, {
                BuildCondition.ALL_ANDROID, BuildCondition.ALL_FUCHSIA,
                BuildCondition.ALL_IOS, BuildCondition.ALL_LINUX,
                BuildCondition.ALL_MAC
            }),
            (BuildCondition.NOT_LINUX, {
                BuildCondition.ALL_ANDROID, BuildCondition.ALL_FUCHSIA,
                BuildCondition.ALL_IOS, BuildCondition.ALL_WINDOWS,
                BuildCondition.ALL_MAC
            }),
            (BuildCondition.NOT_MAC, {
                BuildCondition.ALL_ANDROID, BuildCondition.ALL_FUCHSIA,
                BuildCondition.ALL_IOS, BuildCondition.ALL_WINDOWS,
                BuildCondition.ALL_LINUX
            })
        ]
        cpu_combinations: list[tuple[BuildCondition, set[BuildCondition]]] = [
            (BuildCondition.ALL_X86,
             build_conds_matching(_RUSTC_ARCH_MATCH_X86)),
            (BuildCondition.ALL_X64,
             build_conds_matching(_RUSTC_ARCH_MATCH_X64)),
            (BuildCondition.ALL_ARM32,
             build_conds_matching(_RUSTC_ARCH_MATCH_ARM32)),
            (BuildCondition.ALL_ARM64,
             build_conds_matching(_RUSTC_ARCH_MATCH_ARM64)),
        ]

        to_remove: set[BuildCondition] = set()

        for (combined, all_individual) in os_combinations:
            if modes & all_individual == all_individual:
                modes.add(combined)
                to_remove.update(all_individual)

        for (combined, all_individual) in os_merges:
            if modes & all_individual == all_individual:
                modes.add(combined)
                to_remove.update(all_individual)

        for (combined, all_individual) in cpu_combinations:
            # Only add cpu-specific things if it would add something new, if the
            # individual archs are not already covered by combined
            # `BuildCondition`s.
            if all_individual & to_remove != all_individual:
                if modes & all_individual == all_individual:
                    modes.add(combined)
                    to_remove.update(all_individual)

        for r in to_remove:
            modes.remove(r)

        return modes

    def __bool__(self) -> bool:
        """Whether the BuildConditionSet has any conditions."""
        return bool(self.arch_set)

    def __repr__(self) -> str:
        """A string representation of a `BuildConditionSet`."""
        return "BuildConditionSet({})".format(repr(self.arch_set))

    def __eq__(self, other: BuildConditionSet) -> bool:
        """Whether two sets cover the same BUILD file configurations."""
        return self.arch_set == other.arch_set

    @staticmethod
    def ALL() -> BuildConditionSet:
        """A set that covers all BUILD file configurations."""
        return BuildConditionSet(ArchSet.ALL())

    @staticmethod
    def EMPTY():
        """An empty set that represents never being true."""
        return BuildConditionSet(ArchSet.EMPTY())


# Internal representations used by ArchSet.
#
# This is a set of compiler targets known by rustc that we support. The full
# list is from `rustc --print target-list`.
#
# For each compiler target, we have a map to a BuildCondition which would
# represent the BUILD file condition that is true for the compiler target.
#
# NOTE: If this changes, then also update the `BuildConditionSet._optimize()``
# method that combines the `BuildCondition`s. Also update the matchers for sets
# of compiler targets, such as `_RUSTC_ARCH_MATCH_ANDROID`, below.
_RUSTC_ARCH_TO_BUILD_CONDITION = {
    "i686-linux-android": BuildCondition.ANDROID_X86,
    "x86_64-linux-android": BuildCondition.ANDROID_X64,
    "armv7-linux-androideabi": BuildCondition.ANDROID_ARM,
    "aarch64-linux-android": BuildCondition.ANDROID_ARM64,
    # Not supported by rustc but is in //build/config/android/abi.gni
    #   "mipsel-linux-android",
    # Not supported by rustc but is in //build/config/android/abi.gni.
    #   "mips64el-linux-android",
    "aarch64-fuchsia": BuildCondition.FUCHSIA_ARM64,
    "x86_64-fuchsia": BuildCondition.FUCHSIA_X64,
    "aarch64-apple-ios": BuildCondition.IOS_ARM64,
    "armv7-apple-ios": BuildCondition.IOS_ARM,
    "x86_64-apple-ios": BuildCondition.IOS_X64,
    "i386-apple-ios": BuildCondition.IOS_X86,
    # The winapi crate has dependencies that only exist on the "-gnu" flavour of
    # these windows targets. We would like to believe that we don't need them if
    # we are building with MSVC, or with clang which is pretending to be MSVC in
    # the Chromium build. If we get weird linking errors due to missing Windows
    # things in winapi, then we should probably change these to "-gnu".
    "i686-pc-windows-msvc": BuildCondition.WINDOWS_X86,
    "x86_64-pc-windows-msvc": BuildCondition.WINDOWS_X64,
    "i686-unknown-linux-gnu": BuildCondition.LINUX_X86,
    "x86_64-unknown-linux-gnu": BuildCondition.LINUX_X64,
    "x86_64-apple-darwin": BuildCondition.MAC_X64,
    "aarch64-apple-darwin": BuildCondition.MAC_ARM64,
}

# Regexs that will match all related architectures, and no unrelated ones.
_RUSTC_ARCH_MATCH_ANDROID = r"-android"
_RUSTC_ARCH_MATCH_FUCHSIA = r"-fuchsia"
_RUSTC_ARCH_MATCH_IOS = r"-apple-ios"
_RUSTC_ARCH_MATCH_WINDOWS = r"-windows"
_RUSTC_ARCH_MATCH_LINUX = r"-linux-gnu"
_RUSTC_ARCH_MATCH_MAC = r"-apple-darwin"
_RUSTC_ARCH_MATCH_X86 = r"^i[36]86-"
_RUSTC_ARCH_MATCH_X64 = r"^x86_64-"
_RUSTC_ARCH_MATCH_ARM32 = r"^armv7-"
_RUSTC_ARCH_MATCH_ARM64 = r"^aarch64-"
